16.3 分配

为对象分配内存须区分是在栈还是堆上完成。通常情况下,编译器有责任尽可能使用寄存器和栈来存储对象,这有助于提升性能,减少垃圾回收器的压力。

但千万不要以为用了new函数就一定会分配在堆上,即便是相同的源码也有不同的结果。

test.go

package main

import()

func test() *int{ x:=new(int) *x=0xAABB return x }

func main() { println(*test()) }

当编译器禁用内联优化时,所生成代码和我们的源码表面上预期一致。

$go build-gcflags”-l” -o test test.go // 关闭内联优化

$go tool objdump-s”main.test”test

TEXT main.test(SB)test.go test.go:5 SUBQ0xaabb,0(AX) test.go:8 MOVQ AX,0x18(SP) test.go:8 ADDQ$0x10,SP test.go:8 RET

但当使用默认参数时,函数test会被main内联,此时结果就变得不同了。

$go build-o test test.go // 默认优化

$go tool objdump-s”main.main”test

TEXT main.main(SB)test.go test.go:11 SUBQ0x0,0x10(SP) test.go:12 LEAQ 0x10(SP),BX test.go:12 MOVQ0x18,SP test.go:13 RET

看不懂汇编没关系,但显然内联优化后的代码没有调用newobject在堆上分配内存。

编译器这么做,道理很简单。没有内联时,需要在两个栈帧间传递对象,因此会在堆上分配而不是返回一个失效栈帧里的数据。而当内联后,它实际上就成了main栈帧内的局部变量,无须去堆上操作。

Go编译器支持逃逸分析(escape analysis),它会在编译期通过构建调用图来分析局部变量是否会被外部引用,从而决定是否可直接分配在栈上。

编译参数-gcflags”-m”可输出编译优化信息,其中包括内联和逃逸分析。

你或许见过“Zero Garbage”这个说法,其目的就是避免在堆上的分配行为,从而减小垃圾回收压力,提升性能。另外,做性能测试时使用go test-benchmem参数可以输出堆分配次数统计。

好了,本章要关注的是内存分配器,而非编译器。借着上面这个例子,我们开始深入挖掘newobject具体是如何为对象分配内存的。

mcache.go

type mcache struct{ //Allocator cache for tiny objects w/o pointers. tiny unsafe.Pointer tinyoffset uintptr

alloc[_NumSizeClasses]*mspan }

malloc.go

// 内置函数new实现 func newobject(typ*_type)unsafe.Pointer{ return mallocgc(uintptr(typ.size),typ,flags) }

func mallocgc(size uintptr,typ*_type,flags uint32)unsafe.Pointer{ // 当前线程所绑定的cache c:=gomcache()

// 小对象 if sizemaxSmallSize{ // 无须扫描非指针微小对象(小于16) if flags&flagNoScan!=0&&size<maxTinySize{ off:=c.tinyoffset

     // 对齐,调整偏移量 
    if size&7==0{ 
        off=round(off,8) 
     }else if size&3==0{ 
        off=round(off,4) 
     }else if size&1==0{ 
        off=round(off,2) 
     } 

     // 如果剩余空间足够... 
    if off+size<=maxTinySize&&c.tiny!=nil{ 
         // 返回指针,调整偏移量为下次分配做好准备 
        x=add(c.tiny,off) 
        c.tinyoffset=off+size
        return x
     } 

     // 获取新的tiny块 
     // 就是从sizeclass=2的span.freelist获取一个16字节object
    s=c.alloc[tinySizeClass] 
    v:=s.freelist

     // 如果没有可用的object,那么需要从central获取新的span
    if v.ptr() ==nil{ 
        systemstack(func() { 
            mCache_Refill(c,tinySizeClass) 
         }) 

         // 重新提取tiny块 
        s=c.alloc[tinySizeClass] 
        v=s.freelist
     } 

     // 提取object后,调整span.freelist链表,增加使用计数 
    s.freelist=v.ptr().next
    s.ref++ 

     // 初始化(零值)tiny块 
    x=unsafe.Pointer(v) 
     (*[2]uint64)(x)[0] =0
     (*[2]uint64)(x)[1] =0

     // 对比新旧两个tiny块的剩余空间 
     // 新块分配后,其tinyoffset=size,因此比对偏移量即可 
    if size<c.tinyoffset{ 
         // 用新块替换 
        c.tiny=x
        c.tinyoffset=size
     } 

     // 消费一个新的完整tiny块 
    size=maxTinySize
 }else{ 
     // 普通小对象 

     // 查表,以确定sizeclass
    var sizeclass int8
    if size<=1024-8{ 
        sizeclass=size_to_class8[(size+7)>>3] 
     }else{ 
        sizeclass=size_to_class128[(size-1024+127)>>7] 
     } 
    size=uintptr(class_to_size[sizeclass]) 

     // 从对应规格的span.freelist提取object
    s=c.alloc[sizeclass] 
    v:=s.freelist

     // 没有可用的object,从central获取新的span
    if v.ptr() ==nil{ 
        systemstack(func() { 
            mCache_Refill(c,int32(sizeclass)) 
         }) 
         
         // 重新提取object
        s=c.alloc[sizeclass] 
        v=s.freelist
     } 

     // 调整span.freelist链表,增加使用计数 
    s.freelist=v.ptr().next
    s.ref++ 

     // 清零(变量默认总是初始化为零值) 
    x=unsafe.Pointer(v) 
    if flags&flagNoZero==0{ 
        v.ptr().next=0
        if size>2*ptrSize&& ((*[2]uintptr)(x))[1] !=0{ 
            memclr(unsafe.Pointer(v),size) 
         } 
     } 
 } 

}else{ // 大对象直接从heap分配span var s*mspan systemstack(func() { s=largeAlloc(size,uint32(flags)) })

 //span.start实际由address>>pageShift生成 
x=unsafe.Pointer(uintptr(s.start<<pageShift)) 
size=uintptr(s.elemsize) 

}

// 在bitmap做标记 … // 检查触发条件,启动垃圾回收 …

return x }

整理一下这段代码的基本思路:

  • 大对象直接从heap获取span。
  • 小对象从cache.alloc[sizeclass].freelist获取object。
  • 微小对象组合使用cache.tiny object。

对微小对象的处理很有意思。首先,它不能是指针,因为多个小对象被组合到一个object里,显然无法应对垃圾扫描。其次,它从span.freelist获取一个16字节的object,然后利用偏移量来记录下一次分配的位置。

这里有个小细节,体现了作者的细心。当tiny因剩余空间不足而使用新object时,会比较新旧两个tiny object的剩余空间,而非粗暴地喜新厌旧。

分配算法本身并不复杂,没什么好说的,接下来要关注的自然是资源不足时如何扩张。考虑到大对象分配过程没有central这个中间环节,所以我们先跳largeAlloc这个坑。

malloc.go

func largeAlloc(size uintptr,flag uint32) *mspan{ // 计算所需页数 npages:=size>> _PageShift if size&_PageMask!=0{ npages++ }

// 清理(sweep)垃圾 …

// 从heap获取span,并重置在bitmap里的标记 s:=mHeap_Alloc(&mheap_,npages,0,true,flag&_FlagNoZero==0) heapBitsForSpan(s.base()).initSpan(s.layout())

return s }

先不用跟过去看mHeap_Alloc,因为小对象扩张函数mCache_Refill最终也会调用它。

mcache.go

func mCache_Refill(c*mcache,sizeclass int32) *mspan{ // 放弃当前正在使用的span(尚在central.empty里) s:=c.alloc[sizeclass] if s!= &emptymspan{ s.incache=false // 取消“正在使用”标志 }

// 从central获取span进行替换 s=mCentral_CacheSpan(&mheap_.central[sizeclass].mcentral) c.alloc[sizeclass] =s return s }

在跳转到central之前,先得了解sweepgen这个概念。垃圾回收每次都会累加这个类似代龄的计数值,而每个等待处理的span也有该属性。

mheap.go

type mheap struct{ sweepgen uint32 //sweep generation,see comment in mspan sweepdone uint32 //all spans are swept }

type mspan struct{ //if sweepgenh->sweepgen-2,the span needs sweeping //if sweepgenhsweepgen-1,the span is currently being swept //if sweepgen==hsweepgen, the span is swept and ready to use //hsweepgen is incremented by 2 after every GC sweepgen uint32 }

在heap里闲置的span不会被垃圾回收器关注,但central里的span却有可能正在被清理。所以当cache从central提取span时,该属性值就非常重要。

mcentral.go

type mcentral struct{ nonempty mspan // 链表:span尚有空闲object可用 empty mspan // 链表:span没有空闲object可用,或已被cache取走 }

func mCentral_CacheSpan(c*mcentral) *mspan{ // 清理(sweep)垃圾 …

sg:=mheap_.sweepgen

retry: // 遍历nonempty链表 for s=c.nonempty.next;s!= &c.nonempty;s=s.next{ // 需要清理的span if s.sweepgen==sg-2&&cas(&s.sweepgen,sg-2,sg-1) { // 因为要交给cache使用,所以转移到empty链表 mSpanList_Remove(s) mSpanList_InsertBack(&c.empty,s)

     // 垃圾清理 
    mSpan_Sweep(s,true) 
    goto havespan
 } 

 // 忽略正在清理的span
if s.sweepgen==sg-1{ 
    continue
 } 

 // 已清理过的span
mSpanList_Remove(s) 
mSpanList_InsertBack(&c.empty,s) 

goto havespan

}

// 遍历empty链表 for s=c.empty.next;s!= &c.empty;s=s.next{ // 需要清理的span if s.sweepgen==sg-2&&cas(&s.sweepgen,sg-2,sg-1) { mSpanList_Remove(s) mSpanList_InsertBack(&c.empty,s) mSpan_Sweep(s,true)

     // 清理后有可用的object
    if s.freelist.ptr() !=nil{ 
        goto havespan
     } 

     // 清理后依然没可用的object,重试 
    goto retry
 } 

 // 忽略正在清理的span
if s.sweepgen==sg-1{ 
    continue
 } 

 // 已清理过,且不为空的span都被转移到noempty链表 
 // 这里剩下的自然都是全空或正在被cache使用的,继续循环已没有意义 
break

}

// 如果两个链表里都没span可用,扩张 s=mCentral_Grow(c) // 新span将被cache使用,所以放到empty链表尾部 mSpanList_InsertBack(&c.empty,s)

havespan: // 设置被cache使用标志 s.incache=true

return s }

可以看出,从central里获取span时,优先取用已有资源,哪怕是要执行清理操作。只有当现有资源都无法满足时,才去heap获取span,并重新切分成object链表。

mcentral.go

func mCentral_Grow(c*mcentral) *mspan{ // 查表获取所需页数 npages:=uintptr(class_to_allocnpages[c.sizeclass]) size:=uintptr(class_to_size[c.sizeclass])

// 计算切分object数量 n:= (npages<< _PageShift) /size

// 从heap获取span s:=mHeap_Alloc(&mheap_,npages,c.sizeclass,false,true)

// 切分成object链表 p:=uintptr(s.start<< _PageShift) // 内存地址 head:=gclinkptr(p) tail:=gclinkptr(p) for i:=uintptr(1);i<n;i++ { p+=size tail.ptr().next=gclinkptr(p) tail=gclinkptr(p) } tail.ptr().next=0 s.freelist=head

// 重置在bitmap里的标记 heapBitsForSpan(s.base()).initSpan(s.layout())

return s }

好了,现在大小对象殊途同归,都到了mHeap_Alloc这里。

mheap.go

type mheap struct{ busy [_MaxMHeapList]mspan // 链表数组:已分配大对象span,127页以内 busylarge mspan // 链表:已分配超过127页大对象span }

func mHeap_Alloc(h*mheap,npage uintptr,sizeclass int32,large bool,needzero bool) *mspan{ systemstack(func() { s=mHeap_Alloc_m(h,npage,sizeclass,large) })

// 对span清零

return s }

func mHeap_Alloc_m(h*mheap,npage uintptr,sizeclass int32,large bool) *mspan{

// 清理(sweep)垃圾 …

// 从heap获取指定页数的span s:=mHeap_AllocSpanLocked(h,npage) if s!=nil{ // 重置span状态 atomicstore(&s.sweepgen,h.sweepgen) s.state= _MSpanInUse s.freelist=0 s.ref=0 s.sizeclass=uint8(sizeclass) …

 // 小对象取用的span被存放到central.empty链表 
 // 而大对象所取用的span则放在heap.busy链表 
if large{ 
     // 根据页数来判断将其放到busy还是busylarge链表 
     // 数组free使用页数作为索引,那么len(free)就是最大页数边界 
    if s.npages<uintptr(len(h.free)) { 
        mSpanList_InsertBack(&h.busy[s.npages],s) 
     }else{ 
        mSpanList_InsertBack(&h.busylarge,s) 
     } 
 } 

}

return s }

从heap获取span的算法核心是找到大小最合适的块。首先从页数相同的链表查找,如没有结果,再从页数更多的链表提取,直至超大块或申请新块。

如返回更大的span,为避免浪费,会将多余部分切出来重新放回heap链表。同时还尝试合并相邻的闲置span空间,减少碎片。

mheap.go

type mheap struct{ free [_MaxMHeapList]mspan // 链表数组:页数127以内的闲置span freelarge mspan // 链表:页数大于127的闲置span }

func mHeap_AllocSpanLocked(h*mheap,npage uintptr) *mspan{ // 先尝试获取指定页数的span,不行就试页数更多的 for i:=int(npage);i<len(h.free);i++ { // 从链表取span if!mSpanList_IsEmpty(&h.free[i]) { s=h.free[i].next goto HaveSpan } }

// 再不行,就试一试页数超过127的超大span s=mHeap_AllocLarge(h,npage)

// 还没有,就得从操作系统申请新的了 if s==nil{ if!mHeap_Grow(h,npage) { return nil }

 // 因为每次申请最小1MB/128Pages,所以被放到freelarge链表,再试 
s=mHeap_AllocLarge(h,npage) 
if s==nil{ 
    return nil
 } 

}

HaveSpan: // 从free链表移除 mSpanList_Remove(s)

// 如果该span曾被释放物理内存,重新映射补回 …

// 如果该span页数多于预期 … if s.npages>npage{ // 创建新span用来管理多余的内存 t:= (*mspan)(fixAlloc_Alloc(&h.spanalloc)) mSpan_Init(t,s.start+pageID(npage),s.npages-npage)

 // 调整切割后的页数 
s.npages=npage

 // 将新建span放回heap
mHeap_FreeSpanLocked(h,t,false,false,s.unusedsince) 

}

// 在spans填充全部指针 p:=uintptr(s.start) p-= (uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift) for n:=uintptr(0);n<npage;n++ { h_spans[p+n] =s }

return s }

因为freelarge只是一个简单链表,没有页数做索引,也不曾按大小排序,所以只能遍历整个链表,然后选出最小、地址最靠前的块。

mheap.go

func mHeap_AllocLarge(h*mheap,npage uintptr) *mspan{ return bestFit(&h.freelarge,npage,nil) }

func bestFit(listmspan,npage uintptr,bestmspan) *mspan{ for s:=list.next;s!=list;s=s.next{ if s.npages<npage{ continue } if bestnil||s.npagesbest.npages&&s.start<best.start) { best=s } } return best }

至于将span放回heap的mHeap_FreeSpanLocked操作,将在内存回收章节再做详述。而在内存分配阶段,也只剩向操作系统申请新内存块了。

mheap.go

func mHeap_Grow(h*mheap,npage uintptr)bool{ // 大小总是64KB的倍数,最少1MB npage=round(npage, (64<<10)/_PageSize) ask:=npage<< _PageShift if ask< _HeapAllocChunk{ ask= _HeapAllocChunk }

// 向操作系统申请内存 v:=mHeap_SysAlloc(h,ask)

// 创建span用来管理刚申请的内存 s:= (*mspan)(fixAlloc_Alloc(&h.spanalloc)) mSpan_Init(s,pageID(uintptr(v)>>_PageShift),ask>>_PageShift)

// 填充在spans区域的信息 p:=uintptr(s.start) p-= (uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift) for i:=p;i<p+s.npages;i++ { h_spans[i] =s }

// 放到heap相关链表中 mHeap_FreeSpanLocked(h,s,false,true,0)

return true }

依然是用mmap从指定位置申请内存。最重要的是同步扩张bitmap和spans区域,以及调整arena_used这个位置指示器。

malloc.go

func mHeap_SysAlloc(h*mheap,n uintptr)unsafe.Pointer{ // 不能超出arena大小限制 if nuintptr(h.arena_end)-uintptr(h.arena_used) { // 从指定位置申请内存 p:=h.arena_used sysMap((unsafe.Pointer)(p),n,h.arena_reserved, &memstats.heap_sys)

 // 同步扩张bitmap和spans内存 

mHeap_MapBits(h,p+n) mHeap_MapSpans(h,p+n)

 // 调整下一次申请地址 
h.arena_used=p+n

return(unsafe.Pointer)(p) 

} }

mem_linux.go

func sysMap(v unsafe.Pointer,n uintptr,reserved bool,sysStat*uint64) { if!reserved{ p:=mmap_fixed(v,n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1,0) return }

p:=mmap(v,n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1,0) }

至此,内存分配操作流程正式结束。